// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

using Tierless.Framework.Data;
using Tierless.Framework.Entities;
using Tierless.Framework.Constraints;

namespace Tierless.Framework.EntityTypes
{
	/// <summary>
	/// Provides a static cache for <see cref="EntityPropertyDescriptor"/> instances by entity type.
	/// </summary>
	/// <remarks>
	/// To cleanly support binding in a WPF context, the <see cref="Entity"/> class implements <see cref="ICustomTypeDescriptor"/> to
	/// provide the property descriptors for the instance. This allows the property path in WPF to reference the columns of the entity
	/// directly (e.g. CustomerEntity.Name instead of having to use CustomerEntity[Name].AsString).
	/// </remarks>
	public static class EntityPropertyDescriptorCache
	{
		private static Dictionary<string, PropertyDescriptorCollection> _cache = new Dictionary<string, PropertyDescriptorCollection>();

		/// <summary>
		/// Returns the property descriptors for the given entity type and column list.
		/// </summary>
		/// <param name="entityType">The type of the entity.</param>
		/// <param name="columns">The list of columns to be described.</param>
		/// <returns>A collection of <see cref="EntityPropertyDescriptor"/> instances describing the properties of the entity.</returns>
		public static PropertyDescriptorCollection GetPropertyDescriptors(EntityType entityType, EntityColumnCollection columns)
		{
			lock (_cache)
			{
				PropertyDescriptorCollection propertyDescriptors;
				if (!_cache.TryGetValue(entityType.Name, out propertyDescriptors))
				{
					var descriptors = new PropertyDescriptor[entityType.ColumnTypes.Count + entityType.References.Count + 1];

					EntityTypeColumn columnType;
					EntityColumn column;
					PropertyInfo bindingPropertyInfo;
					for (var index = 0; index < entityType.ColumnTypes.Count; index++)
					{
						columnType = entityType.ColumnTypes[index];
						column = columns[index];
						bindingPropertyInfo = column.GetType().GetProperty(column.BindingPropertyName);
						descriptors[index] =
							new EntityColumnPropertyDescriptor
							(
								columnType.Name,
								bindingPropertyInfo,
								column.BindingPropertyName == "Value"
									? column.BindingType
									: bindingPropertyInfo.PropertyType
							);
					}

					EntityReference reference;
					for (var index = 0; index < entityType.References.Count; index++)
					{
						reference = entityType.References[index];
						descriptors[entityType.ColumnTypes.Count + index] =
							reference.Kind == ReferenceKind.Lookup
								? new EntityLookupPropertyDescriptor(reference.PropertyName, reference.Name)
								: (PropertyDescriptor) new EntityChildPropertyDescriptor(reference.PropertyName, reference.Name);
					}

					descriptors[descriptors.Length - 1] = TypeDescriptor.GetProperties(typeof (Entity)).Find("Columns", false);

					propertyDescriptors = new PropertyDescriptorCollection(descriptors, true);
					_cache.Add(entityType.Name, propertyDescriptors);
				}

				return propertyDescriptors;
			}
		}
	}

	/// <summary>
	/// Provides the abstract base class for entity property descriptors.
	/// </summary>
	public abstract class EntityPropertyDescriptor : PropertyDescriptor
	{
		private Type _type;

		/// <summary>
		/// Initializes a new instance of the EntityPropertyDescriptor class.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		/// <param name="type">The type of the property.</param>
		public EntityPropertyDescriptor(string propertyName, Type type)
			: base(propertyName, null)
		{
			if (type == null)
				throw new ArgumentException("EntityPropertyDescriptor must have a type specified.");

			_type = type;
		}

		public override bool CanResetValue(object component)
		{
			return false;
		}

		public override Type ComponentType
		{
			get { return typeof (Entity); }
		}

		public override bool IsReadOnly
		{
			get { return false; }
		}

		public override Type PropertyType
		{
			get { return _type; }
		}

		public override bool ShouldSerializeValue(object component)
		{
			return false;
		}
	}

	/// <summary>
	/// Represents the description of a column-level entity property.
	/// </summary>
	public class EntityColumnPropertyDescriptor : EntityPropertyDescriptor
	{
		/// <summary>
		/// Initializes a new instance of the EntityColumnPropertyDescriptor class.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		/// <param name="bindingPropertyInfo">The reflection info for the column indexer property of the entity.</param>
		/// <param name="type">The type of the property.</param>
		public EntityColumnPropertyDescriptor(string propertyName, PropertyInfo bindingPropertyInfo, Type type)
			: base(propertyName, type)
		{
			_bindingPropertyInfo = bindingPropertyInfo;
		}

		private PropertyInfo _bindingPropertyInfo;

		/// <summary>
		/// Gets the value of the property using the column indexer of the entity.
		/// </summary>
		/// <param name="component">The entity.</param>
		/// <returns>The EntityColumn for the property.</returns>
		public override object GetValue(object component)
		{
			return _bindingPropertyInfo.GetValue(((Entity) component)[Name], null);
		}

		/// <summary>
		/// Clears the value of the column.
		/// </summary>
		/// <param name="component">The entity.</param>
		public override void ResetValue(object component)
		{
			((Entity) component)[Name].Clear();
		}

		/// <summary>
		/// Sets the value of the column.
		/// </summary>
		/// <param name="component"></param>
		/// <param name="tempValue"></param>
		public override void SetValue(object component, object tempValue)
		{
			try
			{
				_bindingPropertyInfo.SetValue(((Entity) component)[Name], tempValue, null);
			}
			catch (TargetInvocationException exception)
			{
				var entity = (Entity) component;
				var violation = new ConstraintViolation(_bindingPropertyInfo.Name, entity.EntityTypeName, new[] { _bindingPropertyInfo.Name }, new[] { _bindingPropertyInfo.Name }, ViolationSeverity.Error, exception.InnerException.Message);
				entity.Violations.Add(violation);
				throw;
			}
		}
	}

	/// <summary>
	/// Represents a lookup property for an entity.
	/// </summary>
	public class EntityLookupPropertyDescriptor : EntityPropertyDescriptor
	{
		/// <summary>
		/// Initializes a new instance of the EntityLookupPropertyDescriptor class.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		/// <param name="lookupName">The name of the lookup.</param>
		public EntityLookupPropertyDescriptor(string propertyName, string lookupName)
			: base(propertyName, typeof (Entity))
		{
			LookupName = lookupName;
		}

		/// <summary>
		/// Gets the lookup name for the property.
		/// </summary>
		public string LookupName { get; private set; }

		/// <summary>
		/// Gets the lookup entity for the property.
		/// </summary>
		/// <param name="component">The entity.</param>
		/// <returns>The lookup entity for the property.</returns>
		public override object GetValue(object component)
		{
			return ((Entity) component).Lookups[LookupName];
		}

		/// <summary>
		/// Clears the lookup entity reference.
		/// </summary>
		/// <param name="component">The entity.</param>
		public override void ResetValue(object component)
		{
			((Entity) component).Lookups[LookupName] = null;
		}

		/// <summary>
		/// Sets the value of the lookup entity reference.
		/// </summary>
		/// <param name="component">The entity.</param>
		/// <param name="tempValue">The lookup entity.</param>
		public override void SetValue(object component, object tempValue)
		{
			((Entity) component).Lookups[LookupName] = (Entity) tempValue;
		}
	}

	/// <summary>
	/// Represents a child entity collection property for an entity.
	/// </summary>
	public class EntityChildPropertyDescriptor : EntityPropertyDescriptor
	{
		/// <summary>
		/// Initializes a new instance of the EntityChildPropertyDescriptor class.
		/// </summary>
		/// <param name="propertyName">The name of the property.</param>
		/// <param name="childName">The name of the child.</param>
		public EntityChildPropertyDescriptor(string propertyName, string childName)
			: base(propertyName, typeof(EntitySet))
		{
			ChildName = childName;
		}

		/// <summary>
		/// Gets the name of the child.
		/// </summary>
		public string ChildName { get; private set; }

		/// <summary>
		/// Overridden to return true for all instances.
		/// </summary>
		public override bool IsReadOnly
		{
			get { return true; }
		}

		/// <summary>
		/// Gets the <see cref="EntitySet"/> of child entities for the child property.
		/// </summary>
		/// <param name="component">The entity.</param>
		/// <returns>The <see cref="EntitySet"/> of child entities for the child property.</returns>
		public override object GetValue(object component)
		{
			return ((Entity) component).Children[ChildName];
		}

		/// <summary>
		/// Throws a <see cref="NotSupportedException"/>.
		/// </summary>
		/// <param name="component">The entity.</param>
		public override void ResetValue(object component)
		{
			throw new NotSupportedException("The {0} property is read-only. Access the entities within the entity set.");
		}

		/// <summary>
		/// Throws a <see cref="NotSupportedException"/>.
		/// </summary>
		/// <param name="component">The entity.</param>
		/// <param name="tempValue">The new value.</param>
		public override void SetValue(object component, object tempValue)
		{
			throw new NotSupportedException("The {0} property is read-only. Access the entities within the entity set.");
		}
	}
}